package dnl.util.ui.tree;

import java.awt.BorderLayout;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.util.ArrayList;
import java.util.Enumeration;
import java.util.Iterator;
import java.util.List;

import javax.swing.Action;
import javax.swing.JOptionPane;
import javax.swing.JPanel;
import javax.swing.JPopupMenu;
import javax.swing.JScrollPane;
import javax.swing.JTree;
import javax.swing.SwingUtilities;
import javax.swing.event.TreeExpansionEvent;
import javax.swing.event.TreeExpansionListener;
import javax.swing.event.TreeSelectionEvent;
import javax.swing.event.TreeSelectionListener;
import javax.swing.tree.DefaultMutableTreeNode;
import javax.swing.tree.DefaultTreeModel;
import javax.swing.tree.MutableTreeNode;
import javax.swing.tree.TreeNode;
import javax.swing.tree.TreePath;


/**
 * A convenient tree component that delegates a <code>javax.swing.JTree</code>.
 *
 * @author Daniel Or
 * @version 1.0
 */
public class Tree extends JPanel {

  //~ Constructors =============================================================

  /**
   * Constructor for Tree.
   */
  public Tree() {
    super(new BorderLayout());

    model = new DefaultTreeModel(top);
    jtree = new JTree(model);
    jtree.setExpandsSelectedPaths(false);
    jtree.putClientProperty("JTree.lineStyle", "Angled");
    CellRenderer renderer = new CellRenderer();
    jtree.setCellRenderer(renderer);
    jtree.addMouseListener(new PopupTrigger());
    jtree.addTreeExpansionListener(new ExpansionListener());
    jtree.addTreeSelectionListener(new SelectionListener());
    JScrollPane sp = new JScrollPane();
    sp.getViewport().add(jtree);
    this.add(sp, BorderLayout.CENTER);
  }
  

  //~ Methods ==================================================================

	/**
	 * Gets the currently selected <code>TreeNode</code> or <code>null</code>
	 * if none is selected.
	 */
	public TreeItem getSelectedItem(){
		if(jtree.getSelectionPath() == null)
			return null;
		DefaultMutableTreeNode node = getTreeNode(jtree.getSelectionPath());
		if (node.getUserObject() instanceof TreeItem){
			return (TreeItem)node.getUserObject();
		}
		return null;
	}

	/**
	 * Returns the first item on the tree (going from the currently selected item
	 * towards the root) that is an instance of the class clazz.
	 * 
	 * @param clazz
	 * @return TreeItem
	 */
	public TreeItem  getFirstItemOnTreePath(Class<?> clazz){
		if(TreeItem.class.isAssignableFrom(clazz))
		if(jtree.getSelectionPath() == null)
			return null;
		DefaultMutableTreeNode node = getTreeNode(jtree.getSelectionPath());
		
		for( ;node != null ; node = (DefaultMutableTreeNode)node.getParent()){
			Object userObj = node.getUserObject();
			if(clazz.isAssignableFrom(userObj.getClass()))
				return (TreeItem)userObj;
		}
			
		return null;
	}

  /**
   * Refreshes the whole tree.
   */
  public void refreshTree() {
    setEnabled(false);

    try {
      removeAllNodes();

      //loadNode(new DirEntryID("0"), top);
      //assetsTree.expandPath(new TreePath(top.getPath()));
    } catch (Exception e) {
    } finally {
      setEnabled(true);
    }
  }

  /**
   * Adds actions to the tree's popup menus
   *
   * @param action
   */
  public void addAction(Action action) {
    popup.add(action);
  }

  /**
   *
   * @param item 
   */
  public void setTop(TreeItem item) {
    top.setUserObject(item);

    if (item.isExpandable()) {
      top.add(new DefaultMutableTreeNode(new Boolean(true)));
    }
    
    Runnable runnable = new Runnable() {
    	@Override
        public void run() {
          model.reload(top);
          jtree.collapsePath(new TreePath(top.getPath()));
        }
      };

    try {
      SwingUtilities.invokeLater(runnable);
    } catch (Exception ex) {
      SwingUtilities.invokeLater(runnable);
    }
  }

  /**
   *
   * @param listener 
   */
  public void addTreeEventListener(TreeEventListener listener) {
    listeners.add(listener);
  }

  /**
   *
   * @param listener 
   */
  public void removeTreeEventListener(TreeEventListener listener) {
    listeners.remove(listener);
  }
  
  /**
   * 
   */
  public void restoreToLastSelectedPath(){
  	jtree.expandPath(lastSelectedPath);
  }

  private void removeAllNodes() {
    removeAllNodesFromNode(top);
  }

  /**
   * @param treeNode
   */
  protected void removeAllNodesFromNode(TreeNode treeNode) {

    if (treeNode == null) {
      return;
    }

    MutableTreeNode node = (MutableTreeNode) treeNode;

    if (jtree.getModel() instanceof DefaultTreeModel) {
      DefaultTreeModel treeModel = (DefaultTreeModel) jtree.getModel();

      for (int i = (node.getChildCount() - 1); i >= 0; i--) {
        treeModel.removeNodeFromParent((MutableTreeNode) node.getChildAt(i));
      }

      treeModel.nodeStructureChanged(node);
    }
  }

  private void fireTreeEvent(TreeEvent event) {

    for (Iterator<TreeEventListener> iter = listeners.iterator(); iter.hasNext();) {
      TreeEventListener listener = iter.next();
      listener.eventPerformed(event);
    }
  }
  
  /**
   * 
   * @param expand
   */
  public void expandAll() {
  	populateAll();
  	
  	Runnable r = new Runnable() {
  		@Override
  		public void run() {
  			expandAllImpl(true);
  		}
  	};
  	SwingUtilities.invokeLater(r);
  }

  /**
   * 
   * @param expand
   */
  private void expandAllImpl(boolean expand) {
  	TreeNode root = (TreeNode)jtree.getModel().getRoot();
  	
  	// Traverse tree from root
  	expandAll(jtree, new TreePath(root), expand);
  }
  
  private void expandAll(JTree tree, TreePath parent, boolean expand) {
  	// Expansion or collapse must be done bottom-up
  	if (expand) {
  		tree.expandPath(parent);
  	} else {
  		tree.collapsePath(parent);
  	}
  	
  	// Traverse children
  	TreeNode node = (TreeNode)parent.getLastPathComponent();
  	if (node.getChildCount() >= 0) {
  		for (Enumeration<?> e=node.children(); e.hasMoreElements(); ) {
  			TreeNode n = (TreeNode)e.nextElement();
  			TreePath path = parent.pathByAddingChild(n);
  			expandAll(tree, path, expand);
  		}
  	}
  	
  }  
  
  
  public void populateAll(){
  	recursePopulate(top);
  }
  
  private void recursePopulate(DefaultMutableTreeNode dmt){
  	
  	expand(dmt);
  	int size = dmt.getChildCount();
  	
  	for (int i = 0; i < size; i++) {
  		DefaultMutableTreeNode child = (DefaultMutableTreeNode)dmt.getChildAt(i);
  		if(child.getChildCount() > 0){
  			recursePopulate(child);
  		}
  	}
  }  
  

  private boolean expand(final DefaultMutableTreeNode node) {
    DefaultMutableTreeNode flag = null;

    synchronized (node) {
    	if(!node.isLeaf()){
    		flag = (DefaultMutableTreeNode) node.getFirstChild();
    	}
    }

    if (flag == null) { // No flag
      return false;
    }

    Object obj = flag.getUserObject();

    if (!(obj instanceof Boolean)) {
      return false; // Already expanded
    }

    node.removeAllChildren();
    
    TreeItem item = (TreeItem) node.getUserObject();
    TreeItem[] tItems = item.expandImpl();

    if (tItems != null) {

      for (int i = 0; i < tItems.length; i++) {
        DefaultMutableTreeNode newnode = new DefaultMutableTreeNode(tItems[i]);
        node.add(newnode);

        if (tItems[i].isExpandable()) {
          newnode.add(new DefaultMutableTreeNode(new Boolean(true)));
        }
      }
    } else{
    	String message = "Expansion has failed.";
    	JOptionPane.showMessageDialog(this.jtree, message, "Error", JOptionPane.ERROR_MESSAGE);
    }
    

	  Runnable runnable = new Runnable() {
		  @Override
	      public void run() {
	        model.reload(node);
	      }
	    };
	
	  //try {
	  //  SwingUtilities.invokeAndWait(runnable);
	  //} catch (Exception ex) {
	    SwingUtilities.invokeLater(runnable);
	  //}

    return true;
  }
  
  //^^^^^^^^^^^^^^^^ Utility methods ^^^^^^^^^^^^^^^^^^^^^^^^

  /**
   * Gets the node for a given path.
   *
   * @param path
   *
   * @return
   */
  protected static DefaultMutableTreeNode getTreeNode(TreePath path) {
    return (DefaultMutableTreeNode) (path.getLastPathComponent());
  }

  //~ Classes ==================================================================

  /**
   * a mouse listener that triggers the popup menus
   */
  private class PopupTrigger extends MouseAdapter {
    @Override
	public void mouseReleased(MouseEvent e) {

      if (e.isPopupTrigger()) {
        int x = e.getX();
        int y = e.getY();
        TreePath path = jtree.getPathForLocation(x, y);

        if (path != null) {
          DefaultMutableTreeNode dnode = getTreeNode(path);

          if (dnode.equals(top)) {
            return;
          }

          popup.show(jtree, x, y);

          //clickedPath = path;
        }
      }
    }
  }

  private class ExpansionListener implements TreeExpansionListener {
	  @Override
    public void treeCollapsed(TreeExpansionEvent event) {
      final DefaultMutableTreeNode node = getTreeNode(event.getPath());
      Thread runner = new Thread() {
          @Override
		public void run() {
            try {
	            Tree.this.setEnabled(false);
	            TreeItem item = null;
	
	            if (node.getUserObject() instanceof TreeItem) {
	              item = (TreeItem) node.getUserObject();
              	Tree.this.fireTreeEvent(new TreeEvent(TreeEvent.COLLAPSE, item));              
	            }
	
	            removeAllNodesFromNode(node);
	            node.add(new DefaultMutableTreeNode(new Boolean(true)));
            
            } finally {
              Tree.this.setEnabled(true);
            }
          }
        };

      runner.start();
    }

	@Override
    public void treeExpanded(TreeExpansionEvent event) {
      final DefaultMutableTreeNode node = getTreeNode(event.getPath());
      Thread runner = new Thread() {
          @Override
		public void run() {
            try {
	            Tree.this.setEnabled(false);
	            expand(node);
	            TreeItem item = null;
	
	            if (node.getUserObject() instanceof TreeItem) {
	              item = (TreeItem) node.getUserObject();
	              Tree.this.fireTreeEvent(new TreeEvent(TreeEvent.EXPANSION, item));
	            }
            } finally {
              Tree.this.setEnabled(true);
            }
          }
        };

      runner.start();
    }
  }

    /**
   * A listener for selection events
   */
  private class SelectionListener
    implements TreeSelectionListener {

	@Override
    public void valueChanged(TreeSelectionEvent event) {
    	lastSelectedPath = selectedPath;
    	selectedPath = event.getPath();
    	final DefaultMutableTreeNode node = getTreeNode(event.getPath());
      Thread runner = new Thread() {
          @Override
		public void run() {
          	try {
	            Tree.this.setEnabled(false);
	            TreeItem item = null;
	
	            if (node.getUserObject() instanceof TreeItem) {
	              item = (TreeItem) node.getUserObject();
	              Tree.this.fireTreeEvent(new TreeEvent(TreeEvent.SELECTION, item));
	            }
            } finally {
              Tree.this.setEnabled(true);
            }
          }
        };

      runner.start();    	
    }
  }//~ Instance variables =======================================================

  private JTree jtree;
  private DefaultTreeModel model;
  DefaultMutableTreeNode top = new DefaultMutableTreeNode();
  protected JPopupMenu popup = new JPopupMenu();
  private List<TreeEventListener> listeners = new ArrayList<TreeEventListener>();
  private TreePath selectedPath;
  private TreePath lastSelectedPath;
}
